/*
 * Copyright (c) 2022 Diemit <598757652@qq.com>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "it8951.h"
#include "gpio_if.h"
#include "spi_if.h"
#include "esp_err.h"
#include "gpio_types.h"
#include "hdf_log.h"
#include "osal_time.h"
#include "driver/spi_common.h"
#include "driver/spi_master.h"

m5epd_err_t __epdret__;
#define CHECK(x)                  \
    __epdret__ = x;               \
    if (__epdret__ != M5EPD_OK) { \
        return __epdret__;        \
    }

int8_t _pin_cs, _pin_busy, _pin_sck, _pin_mosi, _pin_miso, _pin_rst;

uint8_t _direction, _rotate;

uint16_t _dev_memaddr_l, _dev_memaddr_h;
uint32_t _tar_memaddr;
uint16_t _endian_type, _pix_bpp;

uint16_t _update_count;
uint8_t _is_reverse;

static DevHandle spiHandle = NULL;

void enableEPDPower() { (void)GpioWrite(M5EPD_EPD_PWR_EN_PIN, 1); }
void disableEPDPower() { (void)GpioWrite(M5EPD_EPD_PWR_EN_PIN, 0); }


static void u16tou8(uint16_t u16_data, uint8_t *u8_data)
{
    u8_data[0] = (u16_data >> 8) & 0xFF;
    u8_data[1] = u16_data & 0xFF;
}

static void u32tou8(uint32_t u32_data, uint8_t *u8_data)
{
    u8_data[0] = (u32_data >> 24) & 0xFF;
    u8_data[1] = (u32_data >> 16) & 0xFF;
    u8_data[2] = (u32_data >> 8) & 0xFF;
    u8_data[3] = u32_data & 0xFF;
}

uint8_t GetRotate(void) { return _rotate; }
uint8_t GetDirection(void) { return _direction; }

m5epd_err_t It8951Init(void) {

    _pin_cs   = M5EPD_CS_PIN;
    _pin_busy = M5EPD_BUSY_PIN;
    _pin_rst  = -1;
    _rotate    = IT8951_ROTATE_0;
    _direction = 1;
    _update_count = false;
    _is_reverse   = false;

    (void)GpioSetDir(M5EPD_EPD_PWR_EN_PIN, GPIO_DIR_OUT);
    (void)GpioSetDir(_pin_cs, GPIO_DIR_OUT);
    (void)GpioWrite(_pin_cs, GPIO_VAL_HIGH);
    (void)GpioSetDir(_pin_busy, GPIO_DIR_IN);

    //等待就绪
    osDelay(100);

    //打开电源
    enableEPDPower();

    //等待就绪
    osDelay(1000);

    StartSPI();

    GetSysInfo();

    _tar_memaddr   = 0x001236E0;
    _dev_memaddr_l = 0x36E0;
    _dev_memaddr_h = 0x0012;
    WriteCommand(IT8951_TCON_SYS_RUN);
    WriteReg(IT8951_I80CPCR, 0x0001);  // enable pack write

    // set vcom to -2.30v
    WriteCommand(0x0039);  // tcon vcom set command
    WriteWord(0x0001);
    WriteWord(2300);

    EndSPI();

    //等待就绪
    osDelay(1000);

    HDF_LOGD("Init SUCCESS.");
    
    return M5EPD_OK;
}

/** @brief Invert display colors
 * @param is_reverse 1, reverse color; 0, default
 */
void SetColorReverse(bool is_reverse) {
    _is_reverse = is_reverse;
}

/** @brief Set panel rotation
 * @param rotate direction to rotate.
 * @retval m5epd_err_t
 */
m5epd_err_t SetRotation(uint16_t rotate) {
    if (rotate < 4) {
        _rotate = rotate;
    } else if (rotate < 90) {
        _rotate = IT8951_ROTATE_0;
    } else if (rotate < 180) {
        _rotate = IT8951_ROTATE_90;
    } else if (rotate < 270) {
        _rotate = IT8951_ROTATE_180;
    } else {
        _rotate = IT8951_ROTATE_270;
    }

    if (_rotate == IT8951_ROTATE_0 || _rotate == IT8951_ROTATE_180) {
        _direction = 1;
    } else {
        _direction = 0;
    }
    return M5EPD_OK;
}

/** @brief Clear graphics buffer
 * @param init Screen initialization, If is 0, clear the buffer without
 * initializing
 * @retval m5epd_err_t
 */
m5epd_err_t Clear(bool init) {
    _endian_type = IT8951_LDIMG_L_ENDIAN;
    _pix_bpp     = IT8951_4BPP;
    uint8_t wbuff[4] = {0};

    StartSPI();

    CHECK(SetTargetMemoryAddr(_tar_memaddr));
    if (_direction) {
        CHECK(SetArea(0, 0, M5EPD_PANEL_W, M5EPD_PANEL_H));
    } else {
        CHECK(SetArea(0, 0, M5EPD_PANEL_H, M5EPD_PANEL_W));
    }
    if (_is_reverse) {
        for (uint32_t x = 0; x < ((M5EPD_PANEL_W * M5EPD_PANEL_H) >> 2); x++) {
            GpioWrite(_pin_cs, 0);
            // _epd_spi->write32(0x00000000);
            u32tou8(0x00000000, wbuff);
            (void)SpiWrite(spiHandle, (uint8_t *)wbuff, 4);
            GpioWrite(_pin_cs, 1);
        }
    } else {
        for (uint32_t x = 0; x < ((M5EPD_PANEL_W * M5EPD_PANEL_H) >> 2); x++) {
            GpioWrite(_pin_cs, 0);
            // _epd_spi->write32(0x0000FFFF);
            u32tou8(0x0000FFFF, wbuff);
            (void)SpiWrite(spiHandle, (uint8_t *)wbuff, 4);
            GpioWrite(_pin_cs, 1);
        }
    }

    CHECK(WriteCommand(IT8951_TCON_LD_IMG_END));

    if (init) {
        CHECK(UpdateFull(UPDATE_MODE_INIT));
    }

    EndSPI();

    return M5EPD_OK;
}

/** @brief Write full (960 * 540) 4-bit (16 levels grayscale) image to panel.
 * @param gram pointer to image data.
 * @retval m5epd_err_t
 */
m5epd_err_t WriteFullGram4bpp(const uint8_t *gram) {
    if (_direction) {
        return WritePartGram4bpp(0, 0, M5EPD_PANEL_W, M5EPD_PANEL_H, gram);
    } else {
        return WritePartGram4bpp(0, 0, M5EPD_PANEL_H, M5EPD_PANEL_W, gram);
    }
}

/** @brief Write the image at the specified location, Partial update
 * @param x Update X coordinate, >>> Must be a multiple of 4 <<<
 * @param y Update Y coordinate
 * @param w width of gram, >>> Must be a multiple of 4 <<<
 * @param h height of gram
 * @param gram 4bpp garm data
 * @retval m5epd_err_t
 */
m5epd_err_t WritePartGram4bpp(uint16_t x, uint16_t y, uint16_t w,
                                            uint16_t h, const uint8_t *gram) {
    _endian_type = IT8951_LDIMG_B_ENDIAN;
    _pix_bpp     = IT8951_4BPP;
    uint8_t wbuff[4] = {0};

    // rounded up to be multiple of 4
    if (_direction) {
        x = (x + 3) & 0xFFFC;
    } else {
        x = (x + 3) & 0xFFFC;
        y = (y + 3) & 0xFFFC;
    }

    if (w & 0x03) {
        HDF_LOGE("Gram width %d not a multiple of 4.", w);
        return M5EPD_NOTMULTIPLE4;
    }

    if (_direction) {
        if (x > M5EPD_PANEL_W || y > M5EPD_PANEL_H) {
            HDF_LOGD("Pos (%d, %d) out of bounds.", x, y);
            return M5EPD_OUTOFBOUNDS;
        }
    } else {
        if (x > M5EPD_PANEL_H || y > M5EPD_PANEL_W) {
            HDF_LOGD("Pos (%d, %d) out of bounds.", x, y);
            return M5EPD_OUTOFBOUNDS;
        }
    }

    uint32_t pos = 0;
    // uint64_t length = (w / 2) * h;

    StartSPI();

    uint16_t word = 0;
    CHECK(SetTargetMemoryAddr(_tar_memaddr));
    CHECK(SetArea(x, y, w, h));
    if (_is_reverse) {
        for (uint32_t x = 0; x < ((w * h) >> 2); x++) {
            word = gram[pos] << 8 | gram[pos + 1];

            GpioWrite(_pin_cs, 0);
            // _epd_spi->write32(word);
            u32tou8(word, wbuff);
            (void)SpiWrite(spiHandle, (uint8_t *)wbuff, 4);

            GpioWrite(_pin_cs, 1);
            pos += 2;
        }
    } else {
        for (uint32_t x = 0; x < ((w * h) >> 2); x++) {
            word = gram[pos] << 8 | gram[pos + 1];
            word = 0xFFFF - word;

            GpioWrite(_pin_cs, 0);
            // _epd_spi->write32(word);
            u32tou8(word, wbuff);
            (void)SpiWrite(spiHandle, (uint8_t *)wbuff, 4);

            GpioWrite(_pin_cs, 1);
            pos += 2;
        }
    }
    CHECK(WriteCommand(IT8951_TCON_LD_IMG_END));

    EndSPI();

    return M5EPD_OK;
}


/** @brief Fill the color at the specified location, Partial update
 * @param x Update X coordinate, >>> Must be a multiple of 4 <<<
 * @param y Update Y coordinate
 * @param w width of gram, >>> Must be a multiple of 4 <<<
 * @param h height of gram
 * @param data 4bpp color
 * @retval m5epd_err_t
 */
m5epd_err_t FillPartGram4bpp(uint16_t x, uint16_t y, uint16_t w,
                                           uint16_t h, uint16_t data) {
    _endian_type = IT8951_LDIMG_B_ENDIAN;
    _pix_bpp     = IT8951_4BPP;
    uint8_t wbuff[4] = {0};

    // rounded up to be multiple of 4
    // rounded up to be multiple of 4
    if (_direction) {
        x = (x + 3) & 0xFFFC;
    } else {
        x = (x + 3) & 0xFFFC;
        y = (y + 3) & 0xFFFC;
    }

    if (w & 0x03) {
        HDF_LOGD("Gram width %d not a multiple of 4.", w);
        return M5EPD_NOTMULTIPLE4;
    }

    if (_direction) {
        if (x > M5EPD_PANEL_W || y > M5EPD_PANEL_H) {
            HDF_LOGD("Pos (%d, %d) out of bounds.", x, y);
            return M5EPD_OUTOFBOUNDS;
        }
    } else {
        if (x > M5EPD_PANEL_H || y > M5EPD_PANEL_W) {
            HDF_LOGD("Pos (%d, %d) out of bounds.", x, y);
            return M5EPD_OUTOFBOUNDS;
        }
    }

    // uint64_t length = (w / 2) * h;

    StartSPI();

    CHECK(SetTargetMemoryAddr(_tar_memaddr));
    CHECK(SetArea(x, y, w, h));
    for (uint32_t x = 0; x < ((w * h) >> 2); x++) {
        GpioWrite(_pin_cs, 0);
        // _epd_spi->write32(data);
        u32tou8(data, wbuff);
        (void)SpiWrite(spiHandle, (uint8_t *)wbuff, 4);

        GpioWrite(_pin_cs, 1);

    }
    CHECK(WriteCommand(IT8951_TCON_LD_IMG_END));

    EndSPI();

    return M5EPD_OK;
}

/** @brief Full panel update
 * @param mode update mode
 * @retval m5epd_err_t
 */
m5epd_err_t UpdateFull(m5epd_update_mode_t mode) {
    if (_direction) {
        CHECK(UpdateArea(0, 0, M5EPD_PANEL_W, M5EPD_PANEL_H, mode));
    } else {
        CHECK(UpdateArea(0, 0, M5EPD_PANEL_H, M5EPD_PANEL_W, mode));
    }

    return M5EPD_OK;
}

/** @brief Check if the device is busy
 * @retval m5epd_err_t
 */
m5epd_err_t CheckAFSR(void) {
    uint64_t start_time = OsalGetSysTimeMs();
    while (1) {
        uint16_t infobuf[1];
        CHECK(WriteCommand(IT8951_TCON_REG_RD));
        CHECK(WriteWord(IT8951_LUTAFSR));
        CHECK(ReadWords(infobuf, 1));
        if (infobuf[0] == 0) {
            break;
        }

        if (OsalGetSysTimeMs() - start_time > 3000) {
            HDF_LOGE("Device response timeout.");
            return M5EPD_BUSYTIMEOUT;
        }
    }
    return M5EPD_OK;
}

/** @brief Partial panel update
 * @param x Update X coordinate, >>> Must be a multiple of 4 <<<
 * @param y Update Y coordinate
 * @param w width of gram, >>> Must be a multiple of 4 <<<
 * @param h height of gram
 * @param mode update mode
 * @retval m5epd_err_t
 */
m5epd_err_t UpdateArea(uint16_t x, uint16_t y, uint16_t w,
                                     uint16_t h, m5epd_update_mode_t mode) {
    if (mode == UPDATE_MODE_NONE) {
        return M5EPD_OTHERERR;
    }

    // rounded up to be multiple of 4
    if (_direction) {
        x = (x + 3) & 0xFFFC;
    } else {
        x = (x + 3) & 0xFFFC;
        y = (y + 3) & 0xFFFC;
    }

    CHECK(CheckAFSR());

    if (_direction) {
        if (x + w > M5EPD_PANEL_W) {
            w = M5EPD_PANEL_W - x;
        }
        if (y + h > M5EPD_PANEL_H) {
            h = M5EPD_PANEL_H - y;
        }
    } else {
        if (x + w > M5EPD_PANEL_H) {
            w = M5EPD_PANEL_H - x;
        }
        if (y + h > M5EPD_PANEL_W) {
            h = M5EPD_PANEL_W - y;
        }
    }

    uint16_t args[7];
    switch (_rotate) {
        case IT8951_ROTATE_0: {
            args[0] = x;
            args[1] = y;
            args[2] = w;
            args[3] = h;
            break;
        }
        case IT8951_ROTATE_90: {
            args[0] = y;
            args[1] = M5EPD_PANEL_H - w - x;
            args[2] = h;
            args[3] = w;
            break;
        }
        case IT8951_ROTATE_180: {
            args[0] = M5EPD_PANEL_W - w - x;
            args[1] = M5EPD_PANEL_H - h - y;
            args[2] = w;
            args[3] = h;
            break;
        }
        case IT8951_ROTATE_270: {
            args[0] = M5EPD_PANEL_W - h - y;
            args[1] = x;
            args[2] = h;
            args[3] = w;
            break;
        }
    }

    args[4] = mode;
    args[5] = _dev_memaddr_l;
    args[6] = _dev_memaddr_h;

    StartSPI();
    WriteArgs(IT8951_I80_CMD_DPY_BUF_AREA, args, 7);
    EndSPI();

    _update_count++;

    return M5EPD_OK;
}

/** @brief  Set write area
 * @param x Update X coordinate, >>> Must be a multiple of 4 <<<
 * @param y Update Y coordinate
 * @param w width of gram, >>> Must be a multiple of 4 <<<
 * @param h height of gram
 * @retval m5epd_err_t
 */
m5epd_err_t SetArea(uint16_t x, uint16_t y, uint16_t w,
                                  uint16_t h) {
    uint16_t args[5];
    args[0] = (_endian_type << 8 | _pix_bpp << 4 | _rotate);
    args[1] = x;
    args[2] = y;
    args[3] = w;
    args[4] = h;
    CHECK(WriteArgs(IT8951_TCON_LD_IMG_AREA, args, 5));

    return M5EPD_OK;
}

/** @brief  Write image data to the set address
 * @param data pointer to 4-bpp gram data
 * @retval m5epd_err_t
 */
void WriteGramData(uint16_t data) {
    uint8_t wbuff[4] = {0};

    GpioWrite(_pin_cs, 0);
    //_epd_spi->write32(data);
    u32tou8(data, wbuff);
    (void)SpiWrite(spiHandle, (uint8_t *)wbuff, 4);
    GpioWrite(_pin_cs, 1);
    
}

m5epd_err_t SetTargetMemoryAddr(uint32_t tar_addr) {
    uint16_t h = (uint16_t)((tar_addr >> 16) & 0x0000FFFF);
    uint16_t l = (uint16_t)(tar_addr & 0x0000FFFF);

    WriteReg(IT8951_LISAR + 2, h);
    WriteReg(IT8951_LISAR, l);

    return M5EPD_OK;
}

/** @brief Set power mode to Active
 * @retval m5epd_err_t
 */
m5epd_err_t Active(void) {
    StartSPI();
    CHECK(WriteCommand(IT8951_TCON_SYS_RUN));
    EndSPI();

    return M5EPD_OK;
}

/** @brief Set power mode to StandBy
 * @retval m5epd_err_t
 */
m5epd_err_t StandBy(void) {
    StartSPI();
    CHECK(WriteCommand(IT8951_TCON_STANDBY));
    EndSPI();
    CHECK(WaitBusy());

    return M5EPD_OK;
}

/** @brief Set power mode to Sleep
 * @retval m5epd_err_t
 */
m5epd_err_t Sleep(void) {
    StartSPI();
    CHECK(WriteCommand(IT8951_TCON_SLEEP));
    EndSPI();
    CHECK(WaitBusy());

    return M5EPD_OK;
}

m5epd_err_t WriteReg(uint16_t addr, uint16_t data) {
    WriteCommand(0x0011);  // tcon write reg command
    WriteWord(addr);
    WriteWord(data);
    return M5EPD_OK;
}

m5epd_err_t GetSysInfo(void) {
    uint16_t infobuf[20];
    WriteCommand(IT8951_I80_CMD_GET_DEV_INFO);
    ReadWords(infobuf, 20);
    _dev_memaddr_l = infobuf[2];
    _dev_memaddr_h = infobuf[3];
    _tar_memaddr   = (_dev_memaddr_h << 16) | _dev_memaddr_l;
    HDF_LOGD("memory addr = %04X%04X", _dev_memaddr_h, _dev_memaddr_l);
    return M5EPD_OK;
}

void StartSPI(void) {
    struct SpiDevInfo spiDevinfo = {0};
    spiDevinfo.busNum = VSPI_HOST;  //SPI设备总线号
    spiDevinfo.csNum = 0;           //SPI设备片选号
    spiHandle = SpiOpen(&spiDevinfo);
    if (spiHandle == NULL) {
        HDF_LOGE("SpiOpen: failed\n");
        return;
    }
}

void EndSPI(void) {

    if (spiHandle) {
        SpiClose(spiHandle);
        spiHandle = NULL;
    }
}

m5epd_err_t WaitBusy() {
    uint32_t timeout = 3000;
    uint64_t start_time = OsalGetSysTimeMs();
    uint16_t state = 0;
    while (1) {
        GpioRead(_pin_busy, &state);
        if (state == 1) {
            return M5EPD_OK;
        }

        if (OsalGetSysTimeMs() - start_time > timeout) {
            HDF_LOGE("Device response timeout.");
            return M5EPD_BUSYTIMEOUT;
        }
    }
}

m5epd_err_t WriteCommand(uint16_t cmd) {
    uint8_t wbuff1[2] = {0};
    uint8_t wbuff2[2] = {0};
    u16tou8(0x6000, wbuff1);

    u16tou8(cmd, wbuff2);

    WaitBusy();
    (void)GpioWrite(_pin_cs, GPIO_VAL_LOW);
    (void)SpiWrite(spiHandle, (uint8_t *)wbuff1, 2);

    WaitBusy();
    (void)SpiWrite(spiHandle, (uint8_t *)wbuff2, 2);
    (void)GpioWrite(_pin_cs, GPIO_VAL_HIGH);

    return M5EPD_OK;
}

m5epd_err_t WriteWord(uint16_t data) {

    uint8_t wbuff1[2] = {0};
    uint8_t wbuff2[2] = {0};

    u16tou8(data, wbuff2);

    WaitBusy();
    (void)GpioWrite(_pin_cs, GPIO_VAL_LOW);
    (void)SpiWrite(spiHandle, (uint8_t *)wbuff1, 2);

    WaitBusy();
    (void)SpiWrite(spiHandle, (uint8_t *)wbuff2, 2);
    (void)GpioWrite(_pin_cs, GPIO_VAL_HIGH);

    return M5EPD_OK;
}

m5epd_err_t ReadWords(uint16_t *buf, uint32_t length) {

    uint8_t wbuff[2] = {0};
    uint8_t rbuff[2] = {0};

    u16tou8(0x1000, wbuff);

    // uint16_t dummy;
    WaitBusy();
    GpioWrite(_pin_cs, 0);
    (void)SpiWrite(spiHandle, (uint8_t *)wbuff, 2);

    // dummy
    WaitBusy();
    // _epd_spi->transfer16(0);
    SpiRead(spiHandle, rbuff, 2);
    
    WaitBusy();
    for (size_t i = 0; i < length; i++) {
        // buf[i] = _epd_spi->transfer16(0);
        SpiRead(spiHandle, rbuff, 2);
        buf[i] = (rbuff[0] << 8) | rbuff[1];
    }

    GpioWrite(_pin_cs, 1);
    
    return M5EPD_OK;
}

m5epd_err_t WriteArgs(uint16_t cmd, uint16_t *args,
                                    uint16_t length) {
    WriteCommand(cmd);
    for (uint16_t i = 0; i < length; i++) {
        WriteWord(args[i]);
    }
    return M5EPD_OK;
}

uint16_t UpdateCount(void) { return _update_count; }

void ResetUpdateCount(void) { _update_count = 0; }
